using System;
using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Collections.Generic;
using UnityEngine;

public class TcpConnect : IDisposable
{
    public enum NetStatus
    {
        None,
        DnsParse,
        Connecting,
        Connected,
        DisConnect,
        Shotdown
    }

    public NetStatus status { get; private set; }
    public TcpClient client { get; private set; }
    public IPAddress[] iPAddresses { get; private set; }
    private int port;

    private readonly byte headerSize = sizeof(ushort);

    private object sendlock = new object();
    private Queue<NetPackage> sendPkgs = new Queue<NetPackage>();
    private byte[] sendBuffer = new byte[1<<16];
    private MemoryStream writeStream;
    private BinaryWriter writeBinaryWriter;

    private object recvlock = new object();
    private Queue<NetPackage> recvPkgs = new Queue<NetPackage>();
    private byte[] recvBuffer = new byte[1<<16];
    private int recvLength;
    private MemoryStream readStream;
    private BinaryReader readBinaryReader;

    public event Action<NetPackage> OnHandleRecvMessage;

    private IAsyncResult dnsParse;

    public TcpConnect()
    {
        writeStream = new MemoryStream(sendBuffer);
        writeBinaryWriter = new BinaryWriter(writeStream);
        
        readStream = new MemoryStream(recvBuffer);
        readBinaryReader = new BinaryReader(readStream);

        client = new TcpClient(AddressFamily.InterNetwork);
    }

    public void Connect(string hostNameOrAddress, int port)
    {
        if (status == NetStatus.DnsParse)
            return;

        try
        {
            this.port = port;
            status = NetStatus.DnsParse;
            dnsParse = Dns.BeginGetHostAddresses(hostNameOrAddress, OnDnsParseCallback, this);
        }
        catch (System.Exception e)
        {
            status = NetStatus.None;
            Debug.LogError(e.ToString());
        }
    }

    private void OnDnsParseCallback(IAsyncResult ir)
    {
        try
        {
            iPAddresses = Dns.EndGetHostAddresses(ir);

            if (status == NetStatus.Connecting)
                return;

            status = NetStatus.Connecting;
            client.BeginConnect(iPAddresses[0], port, OnConnectCallback, this);
        }
        catch (System.Exception e)
        {
            status = NetStatus.None;
            Debug.LogError(e.ToString());
        }
    }

    private void OnConnectCallback(IAsyncResult ir)
    {
        try
        {
            client.EndConnect(ir);
            status = NetStatus.Connected;

            client.GetStream().BeginRead(recvBuffer, 0, recvBuffer.Length, OnRecvCallback, this);

            Debug.Log("On Connect Ok!");
        }
        catch (System.Exception e)
        {
            status = NetStatus.None;
            Debug.LogError(e.ToString());
        }
    }

    public void Send(byte[] data)
    {
        if (status == NetStatus.Connected)
        {
            lock(sendlock)
            {
                var npkg = NetPackage.Create(data);
                if (sendPkgs.Count <= 0)
                {
                    writeStream.Seek(0, SeekOrigin.Begin);
                    writeBinaryWriter.Write((ushort)npkg.size);
                    writeBinaryWriter.Write(npkg.data, 0, npkg.size);
                    client.GetStream().BeginWrite(sendBuffer, 0, npkg.size + headerSize, OnSendCallback, this);
                }
                sendPkgs.Enqueue(npkg);
            }
        }
        else
        {
            Debug.LogError("Connection is Not Ready!");
        }
    }

    private void OnSendCallback(IAsyncResult ir)
    {
        client.GetStream().EndWrite(ir);

        lock(sendlock)
        {
            var npkg = sendPkgs.Dequeue();
            npkg.Release();
            if (sendPkgs.Count > 0)
            {
                npkg = sendPkgs.Peek();
                writeStream.Seek(0, SeekOrigin.Begin);
                writeBinaryWriter.Write((ushort)npkg.size);
                writeBinaryWriter.Write(npkg.data, 0, npkg.size);
                client.GetStream().BeginWrite(sendBuffer, 0, npkg.size + headerSize, OnSendCallback, this);
            }
        }
    }

    private void OnRecvCallback(IAsyncResult ir)
    {
        try
        {
            int len = client.GetStream().EndRead(ir);
            if (len > 0)
            {
                recvLength += len;
                if (recvLength > headerSize)
                {
                    var pkgSize = readBinaryReader.ReadUInt16();
                    while (pkgSize <= recvLength - headerSize)
                    {
                        var data = readBinaryReader.ReadBytes(pkgSize);
                        var npkg = NetPackage.Create(data);

                        lock(recvlock) recvPkgs.Enqueue(npkg);

                        recvLength -= pkgSize + headerSize;
                        if (recvLength < headerSize)
                            break;

                        Array.Copy(recvBuffer, pkgSize + headerSize, recvBuffer, 0, recvLength);
                        readStream.Seek(0, SeekOrigin.Begin);

                        pkgSize = readBinaryReader.ReadUInt16();
                    }
                    readStream.Seek(0, SeekOrigin.Begin);
                }
            }
            client.GetStream().BeginRead(recvBuffer, recvLength, recvBuffer.Length - recvLength, OnRecvCallback, this);
        }
        catch (System.Exception e)
        {
            Debug.LogError(e.ToString());
            client?.Close();
        }
    }

    public void UpdateRecvPkgs()
    {
        lock(recvlock)
        {
            while(recvPkgs.Count > 0)
            {
                OnHandleRecvMessage?.Invoke(recvPkgs.Dequeue());
            }
        }
    }

    public void Close()
    {
        status = NetStatus.Shotdown;
        client?.Close();

        writeStream?.Dispose();
        writeBinaryWriter?.Dispose();

        readStream?.Dispose();
        writeStream?.Dispose();
    }

    public void Dispose()
    {
        Close();
    }
}
